﻿using Microsoft.Extensions.Options;
using ProxyServer.Kestrel;
using System.Security.Cryptography.X509Certificates;

namespace ProxyServer.Acme;

public interface ICertificateRepository
{
    Task SaveAsync(string orderId, X509Certificate2 certificate, string? password, CancellationToken cancellationToken);
    Task<X509Certificate2?> GetAsync(string orderId, string? password, CancellationToken cancellationToken);
}

public class FileSystemCertificateRepositoryOptions
{
    public string Directory { get; set; } = "./cert-store";
}

public class FileSystemCertificateRepository : ICertificateRepository
{
    private readonly FileSystemCertificateRepositoryOptions _options;
    private readonly ILogger _logger;
    public FileSystemCertificateRepository(IOptions<FileSystemCertificateRepositoryOptions> options, ILogger<FileSystemCertificateRepository> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    private DirectoryInfo? _dir;
    protected virtual DirectoryInfo Dir
    {
        get
        {
            if(_dir is null)
            {

                if (Directory.Exists(_options.Directory) == false)
                {
                    _dir = Directory.CreateDirectory(_options.Directory);
                }
                else
                {
                    _dir = new DirectoryInfo(_options.Directory);
                }
            }

            return _dir;
        }
    }

    public Task<X509Certificate2?> GetAsync(string orderId, string? password, CancellationToken cancellationToken)
    {
        var filePath = Path.Combine(Dir.FullName, $"{orderId}.pfx");
        if(File.Exists(filePath) == false) { return Task.FromResult((X509Certificate2?)null); }

        var cert = X509CertificateLoader.LoadPkcs12FromFile(filePath, password);

        return Task.FromResult((X509Certificate2?)cert);
    }

    public async Task SaveAsync(string orderId, X509Certificate2 certificate, string? passwrod, CancellationToken cancellationToken)
    {
        var tmpFile = Path.GetTempFileName();

        await File.WriteAllBytesAsync(tmpFile, certificate.Export(X509ContentType.Pfx, passwrod));

        var fileName = orderId + ".pfx";
        var output = Path.Combine(Dir.FullName, fileName);

        // File.Move is an atomic operation on most operating systems. By writing to a temporary file
        // first and then moving it, it avoids potential race conditions with readers.

        File.Move(tmpFile, output);
    }
}

public class X509CertificateStore
{
    private readonly X509Store _x509;
    private readonly ILogger _logger;

    public X509CertificateStore(ILogger<X509CertificateStore> logger) 
    {
        _logger = logger;
        _x509 = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        _x509.Open(OpenFlags.ReadWrite);
    }

    public Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellation)
    {
        try
        {
            _x509.Add(certificate);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to Save certificate");
            throw;
        }

        return Task.CompletedTask;
    }

    public Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(List<string> domains, CancellationToken cancellationToken)
    {
        var domainNames = new HashSet<string>(domains);
        var result = new List<X509Certificate2>();
        var certs = _x509.Certificates.Find(X509FindType.FindByTimeValid,
            DateTime.Now,
            validOnly: true);

        foreach (var cert in certs)
        {
            if (!cert.HasPrivateKey)
            {
                continue;
            }

            foreach (var dnsName in X509CertificateHelpers.GetAllDnsNames(cert))
            {
                if (domainNames.Contains(dnsName))
                {
                    result.Add(cert);
                    break;
                }
            }
        }

        return Task.FromResult(result.AsEnumerable());
    }
}

/// <summary>
/// Loads the ASP.NET Developer certificate
/// /// </summary>
public class DeveloperCertLoader
{
    // see https://github.com/aspnet/Common/blob/61320f4ecc1a7b60e76ca8fe05cd86c98778f92c/shared/Microsoft.AspNetCore.Certificates.Generation.Sources/CertificateManager.cs#L19-L20
    // This is the unique OID for the developer cert generated by VS and the .NET Core CLI
    private const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
    private const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
    private readonly IHostEnvironment _environment;
    private readonly ILogger<DeveloperCertLoader> _logger;

    public DeveloperCertLoader(
        IHostEnvironment environment,
        ILogger<DeveloperCertLoader> logger)
    {
        _environment = environment;
        _logger = logger;
    }

    public Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(CancellationToken cancellationToken)
    {
        if (!_environment.IsDevelopment())
        {
            return Task.FromResult(Enumerable.Empty<X509Certificate2>());
        }

        var certs = FindDeveloperCert();

        return Task.FromResult(certs);
    }

    private IEnumerable<X509Certificate2> FindDeveloperCert()
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);
        var certs = store.Certificates.Find(X509FindType.FindByExtension, AspNetHttpsOid, validOnly: false);
        if (certs.Count == 0)
        {
            _logger.LogDebug("Could not find the " + AspNetHttpsOidFriendlyName);
        }
        else
        {
            _logger.LogDebug("Using the " + AspNetHttpsOidFriendlyName + " for 'localhost' requests");

            foreach (var cert in certs)
            {
                yield return cert;
            }
        }
    }
}